using System;
using Indiefreaks.Xna.Core;
using Indiefreaks.Xna.Physics.Entities;
using Indiefreaks.Xna.Rendering.Camera;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;

namespace Indiefreaks.AceOnSteroids.Entities
{
    /// <summary>
    /// Creates asteroids
    /// </summary>
    /// <remarks>
    /// It's always a good practice to use the factory pattern when you know you'll have multiple instances of a given game entity in your game.
    /// It lets you place in one single class all the code required to create a new instance and reuse them whenever needed.
    /// </remarks>
    public class AsteroidFactory : IContentHost
    {
        private Model _renderModel;
        private Model _collisionModel;
        private readonly Random _dice;

        /// <summary>
        /// Creates a new instance
        /// </summary>
        public AsteroidFactory()
        {
            // we'll use a random generator to create the asteroid instance for its initial position and rotating force
            _dice = new Random();
        }

        #region Implementation of IContentHost

        public void LoadContent(IContentCatalogue catalogue, ContentManager manager)
        {
            _renderModel = manager.Load<Model>("Models/Asteroid");
            _collisionModel = manager.Load<Model>("Models/AsteroidCollision");
        }

        public void UnloadContent(IContentCatalogue catalogue)
        {
        }

        #endregion

        /// <summary>
        /// Creates a new asteroid instance
        /// </summary>
        /// <returns/>
        public Asteroid Create()
        {
            var asteroid = new Asteroid(_renderModel, _collisionModel);
            
            // we create a random angular force and apply it to the asteroid collision entity
            var angularForce = GetRandomAngularForce();
            ((ConvexHullCollisionMove) asteroid.CollisionMove).Entity.ApplyAngularImpulse(ref angularForce);

            // we do the same with our random position
            var position = GetRandomPositionSlightlyOutOfView();
            // we still need to scale the asteroid the size we want (later, we'll be able to add a parameter to create asteroids of multiple sizes
            asteroid.World = Matrix.CreateScale(100f)*Matrix.CreateTranslation(position);

            // and finally, for now, we'll apply a simple linear force pushing the asteroid towards the World origin coordinate
            asteroid.CollisionMove.ApplyObjectForce(-Vector3.Normalize(position));

            Application.SunBurn.ObjectManager.Submit(asteroid);

            return asteroid;
        }

        /// <summary>
        /// This method returns a random Vector3 instance with values between -0.5f and 0.5f.
        /// </summary>
        /// <returns/>
        private Vector3 GetRandomAngularForce()
        {
            float x = (float)(_dice.NextDouble() * 2) - 1;
            float y = (float)(_dice.NextDouble() * 2) - 1;
            float z = (float)(_dice.NextDouble() * 2) - 1;

            return new Vector3(x, y, z);
        }

        /// <summary>
        /// We want to get random 3D positions slightly outside of the screen near the borders.
        /// </summary>
        /// <returns/>
        private Vector3 GetRandomPositionSlightlyOutOfView()
        {
            Viewport viewport = Application.Graphics.GraphicsDevice.Viewport;

            float x = 0f, y = 0f;

            // we randomly roll a 4 side dice to select one of the 4 borders
            var side = _dice.Next(4);

            switch (side)
            {
                case 0: // Left
                    {
                        // if left, we place the asteroid slightly outside of the view on the left of the screen
                        x = viewport.Bounds.Left - 100;
                        // and we randomly pick a y position between the top and bottom values of the screen
                        y = _dice.Next(viewport.Bounds.Top, viewport.Bounds.Bottom);
                        break;
                    }
                case 1: // Right
                    {
                        // for the right side, it's the same except we place the x on the right side
                        x = viewport.Bounds.Right + 100;
                        y = _dice.Next(viewport.Bounds.Top, viewport.Bounds.Bottom);
                        break;
                    }
                case 2: // Top
                    {
                        // for the Top side, it's the opposite, x and y are inverted
                        x = _dice.Next(viewport.Bounds.Left, viewport.Bounds.Right);
                        y = viewport.Bounds.Top - 100;
                        break;
                    }
                case 3: // Bottom
                    {
                        x = _dice.Next(viewport.Bounds.Left, viewport.Bounds.Right);
                        y = viewport.Bounds.Bottom + 100;
                        break;
                    }
                default:
                    break;
            }

            // now that we have the 2D screen position where the asteroid should appear, we can now transform it or rather project it to the 3d world.
            // if you remember well, we did just that in our previous tutorial with the WrappedScreenBehavior. We can therefore copy/paste the maths associated

            // we retrieve the camera
            var camera = Application.SunBurn.GetManager<CameraManager>(true).ActiveCamera;

            Vector3 nearSource = viewport.Unproject(new Vector3(x, y, viewport.MinDepth), camera.SceneState.Projection, camera.SceneState.View, Matrix.Identity);
            Vector3 farSource = viewport.Unproject(new Vector3(x, y, viewport.MaxDepth), camera.SceneState.Projection, camera.SceneState.View, Matrix.Identity);

            Vector3 direction = farSource - nearSource;
            direction.Normalize();

            var ray = new Ray(nearSource, direction);
            var plane = new Plane(Vector3.Up, 0f);

            float denominator = Vector3.Dot(plane.Normal, ray.Direction);
            float numerator = Vector3.Dot(plane.Normal, ray.Position) + plane.D;
            float t = -(numerator / denominator);

            // and now, we can create and return our random position Vector3
            return nearSource + direction*t;
        }
    }
}